iT邦幫忙

2022 iThome 鐵人賽

DAY 22
1
自我挑戰組

跟著 Go 實戰聖經 一起自學 Go系列 第 22

DAY 22 Go 語言 型別檢查

  • 分享至 

  • xImage
  •  

昨天將結構 (struct) 完整的介紹完畢,今天就來介紹如何檢查型別。

在本次的鐵人賽曾多次提到 Go 語言是強型別的語言,也就是遇到函式引數型別和實際叫用型別不符合的情況,是會直接出錯或者編譯失敗。
但是有時候就會遇到型別不一致的時候,這時候可以選擇使用 型別轉換(type conversion) ,將其中一個的值轉成跟另一個值相同的型別:

型別轉換

<型別>(<值>)

但是在做型別轉換時需要特別注意,你轉換的型別會不會可能有問題,例如:若想要將 int64 變數轉成 int8 ,就要特別注意有沒有可能發生溢位的錯誤,因為 int64 的儲存範圍比 int8 大,所以稍不注意就可能發生溢位的錯誤。

以及若是今天想將 float 轉成 int 時,原本 float 後面的小數點便會被無條件捨去。再進行型別轉換時,可能會損失資料(如損失小數點後的位數),但不用擔心這都是合理的行為,且在真實世界也是很常見的 (畢竟也不可能將 float 轉成 int ,然後又威脅人家不能省略小數點後的位數,那乾脆不要轉換?!) ,所以其實只需特別注意溢位問題,就可以安心做轉換啦!

雖然型別轉換是方便的,但也總有不能互相轉換的型別們,正常來說你不能強迫將一個字串型別轉換成數字型別 ; 布林值也不能轉換成數字或是字串型別。但 Go 語言還是有提供 strconv 套件,是個可以將字串轉換成其他型別的方便小工具。

相信之前定義常數變數時,有說過若在定義時省略型別, Go 語言是會自動根據給予的初始值,去推斷型別的,基於這個特性,我們可以先定義一個沒有指定型別的數值常數,然後後續再將他賦值給特定數值型別的變數。

x := true // 會自動推測為 bool 型別
y := 10 // 會自動推測為 int 型別
z := "Go" // 會自動推測為 string 型別

範例 1:

package main

import "fmt"

func change() string{
    var maxInt8 int8 = 127
    greaterThanMaxInt8 := 128
    float := 9.999
    newInt := fmt.Sprintf("maxInt8: %v ,int64: %v\n",maxInt8,int64(maxInt8)) // 將 int8 轉換成型別 int64,因為int64 型別的容量較大,所以可以正常轉換
    newInt += fmt.Sprintf("greaterThanMaxInt8: %v ,int8: %v\n",greaterThanMaxInt8,int8(greaterThanMaxInt8)) // 因為 greaterThanMaxInt8 大於 int8 的最大值,所以會發生越界繞回的問題
    newInt += fmt.Sprintf("maxInt8: %v ,float64: %v\n",maxInt8,float64(maxInt8))
    newInt += fmt.Sprintf("float: %v ,int: %v\n",float,int(float)) // 將 float 轉換為 int 小數點會直接捨去
    return newInt
}

func main() {
    fmt.Println(change())
}

範例 1(執行結果):

maxInt8: 127 ,int64: 127
greaterThanMaxInt8: 128 ,int8: -128
maxInt8: 127 ,float64: 127
float: 9.999 ,int: 9

interface{} 空介面

從開賽至今也已經 20 幾天了,相信大家應該有發現我們經常使用 fmt.Print() 函式來將資料印出,但不知道大家有沒有想過, Go 是強型別語言,若是型別不同是會報錯的,但為什麼我們使用 fmt.Print() 卻不會遇到型別不同的錯誤呢?

根據官方文件的說明,fmt.Print() 內可以接受 interface{} 型別的參數。

func Printf(format string, a ...interface{}) (n int, err error)

至於 介面(interface) 詳細的話會在後面章節詳細說明,我們可以先簡單理解為 介面(interface) 是一個規範,會列出很多個定義的函式(方法),只要任何型別有具備相同的函式(方法),就是是為符合介面的型別,然後去實踐 interface 裡面的方法。我將它想像成介面就像是一個驗票員,只要你有符合的票,那介面就會讓你執行方法。

interface{} 的型別就是個沒有指定任何函數的空型別,它自己也不具備任何欄位。在 Go 語言中無論是內建或是自訂型別,都會符合 interface{} 的型別,這個正是我們前面提出:「為什麼我們使用 fmt.Print() 卻不會遇到型別不同的錯誤」疑問的解答。

當今天想要存取傳入 interface{} 變數的方法時,是沒有辦法的,因為上面有說介面是空型別,裡面也沒有任何方法(但另一方面也是因為如此,空見面才能維持型別的安全性),但若想要挖出被 interface{} 埋沒的資料型別功能,我們就要使用 型別斷言(type assertion) 來將值轉成指定型別並回傳。

型別斷言

型別斷言會將值轉成指定型別並回傳,使用方法如下:

<值> := <變數名稱>.(<型別>)

也可以接收第二個參數,用來回傳轉換是否成功。若不接收第二個參數的回傳值,當轉換失敗時, Go 語言會引發 panic。

<值>, <ok> := <變數名稱>.(<型別>)

簡而言之,當今天想要將值傳入 interface{} 裡,原本值的內容不會消失,但你卻看不見也摸不著,只有使用型別斷言 (type assertion) 才有辦法取值。而在執行型別斷言時, Go 語言編譯時一樣會做型別的檢查,若是檢查發現失效,那就會噴錯,這時處理錯誤訊息就非常重要了,而處理錯誤訊息一樣後面章節會介紹到,這邊就不詳細說明。

範例 2:

package main

import (
	"errors"
	"fmt"
)

// 接收一個 interface{} 型別的函式
func doubler(value interface{}) (string, error) {
    if int, ok := value.(int); ok {
        return fmt.Sprint(int * 2), nil
    }

    if str, ok := value.(string); ok {
        return str + str, nil
    }
    // 型別不符前面的檢查, 傳回錯誤
    return "", errors.New("輸入的值錯誤")
}

func main() {
    res, _ := doubler(5)
    fmt.Println("5 :", res)
    res, _ = doubler("good")
    fmt.Println("good :", res)
    _, err := doubler(true)
    fmt.Println("true :", err)
}

範例 2(執行結果):

5 : 10
good : goodgood
true : 輸入的值錯誤

型別 switch (type switch)

switch <值> := <變數>.(型別) {
case <型別>:
    <敘述>
case <型別>, <型別>:
    <敘述>
default:
    <敘述>
}

型別 switch 會執行符合 case 型別的敘述,且把值設定成指定的型別,但當你在 case 後比對不只一種型別,那他就會不知道要把值指定成哪個型別,所以就還是得在 case 下的敘述加上型別斷言。

範例 3:

package main

import (
	"errors"
	"fmt"
)

func doubler(value interface{}) (string, error) {
    switch t := value.(type){
    case string:
        return t + t, nil
    case bool:
        if t {
            return "truetrue", nil
        }
        return "falsefalse", nil
    case float32, float64:
        if f, ok := t.(float64); ok {
            return fmt.Sprint(f * 2), nil
        }
        return fmt.Sprint(t.(float32)*2), nil
    case int:
        return fmt.Sprint(t * 2),nil
    default:
        return "", errors.New("輸入的值錯誤")
    }
}

func main() {
    res, _ := doubler(-5)
    fmt.Println("-5 :", res)
    res, _ = doubler(5)
    fmt.Println("5 :", res)
    res, _ = doubler("good")
    fmt.Println("good :", res)
    res, _ = doubler(true)
    fmt.Println("true :", res)
    res, _ = doubler(float32(9.99))
    fmt.Println("9.99 :", res)
}

範例 3(執行結果):

-5 : -10
5 : 10
good : goodgood
true : truetrue
9.99 : 19.98

如果我們不需要精確控制型別斷言檢查,利用型別 switch 敘述就能簡化安全型別,但同時可以讓我們掌控型別斷言檢查的結果。

明天要來介紹,如何將程式碼集中成為可以重複使用的元件,也就是函式(function),那我們明天見~


上一篇
DAY 21 Go 語言 幫自訂型別加上自己的函式或方法(method)
下一篇
DAY 23 Go 語言 函式 (function) 介紹及宣告函式
系列文
跟著 Go 實戰聖經 一起自學 Go30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言